<?php  if (!defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Реализация паттерна ActiveRecord
 * Общий предок для всех моделей, реализующих данный паттерн
 *
 * @package CoreFramework
 * @version 1.0
 * @author Gennadiy Kozlenko
 */
class M_Object extends Model {
   
   /**
    * Название таблицы
    *
    * @access protected
    * @var string
    */
   var $_table = '';
   
   /**
    * Массив первичных ключей таблицы
    *
    * @access protected
    * @var array
    */
   var $_primary_keys = array();
   
   /**
    * Массив уникальных ключей таблицы
    *
    * @access protected
    * @var array
    */
   var $_keys = array();
   
   /**
    * Массив идентификаторов, связанных с первичными ключами
    *
    * @access protected
    * @var array
    */
   var $_ids = array();
   
   /**
    * Массив данных из базы
    *
    * @access protected
    * @var array
    */
   var $_values = array();
   
   /**
    * Флаг, симвользтрующий что необходима вставка в базу
    *
    * @access protected
    * @var boolean
    */
   var $_is_insert = true;
   
   /**
    * Флаг, символизирующий что были изменения
    *
    * @access protected
    * @var boolean
    */
   var $_is_changed = false;
   
   /**
    * Конструктор класса
    *
    * @access public
    * @return M_Object
    */
   function M_Object() {
      parent::Model();
      if (empty($this->_table)) {
         show_error('The field "table" is necessarily for filling');
      }
      $this->lang->load('object');
      $this->_load_structure();
   } // end M_Object
   
   /**
    * Установка идентификатора/идентификаторов
    * В качестве аргументов принимает список значений первичных ключей
    * (в том порядке, в каком они объявлены в таблице) 
    * или же ассоциативный массив, поля которого обозначают поля,
    * а значения - идентификаторы.
    * Если поле не задано, то идентификатором считается 0 (ноль)
    *
    * @access public
    * @param mixed
    */
   function set_id() {
      $ids = array();
      foreach ($this->_primary_keys as $key) {
         $ids[$key] = 0;
      }
      if (0 < func_num_args()) {
         if (is_array(func_get_arg(0))) {
            // Пришел массив
            foreach (func_get_arg(0) as $key => $val) {
               if (isset($ids[$key])) {
                  $ids[$key] = $val;
               }
            }
         } else {
            // Пришел список
            $i = 0;
            foreach ($ids as $key => $val) {
               if ($i < func_num_args()) {
                  $val = func_get_arg($i++);
               }
               $ids[$key] = $val;
            }
         }
      }
      $this->_ids = $ids;
      $this->_load_data();
   } // end set_id
   
   /**
    * Получение идентификатора/идентификаторов
    * Если задано поле, то возвращает идентификатор конкретного поля
    * Иначе - ассоциативный массив полей и их идентификаторов
    *
    * @access public
    * @param string $filed
    * @return mixed
    */
   function get_id($filed = '') {
      if (isset($this->_ids[$filed])) {
         return $this->_ids[$filed];
      }
      return $this->_ids;
   } // end get_id
   
   /**
    * Установка значения полю
    *
    * @access public
    * @param string $name
    * @param mixed $value
    * @return boolean
    */
   function set($name, $value) {
      if (isset($this->_values[$name]) && !$this->_values[$name]['auto_increment']) {
         if ($value !== $this->_values[$name]['value']) {
            $this->_values[$name]['value'] = $value;
            $this->_values[$name]['is_changed'] = true;
            $this->_is_changed = true;
         }
         return true;
      }
      return false;
   } // end set
   
   /**
    * Получение значения поля
    *
    * @access public
    * @param string $name
    * @return mixed
    */
   function get($name) {
      if (isset($this->_values[$name])) {
         return $this->_values[$name]['value'];
      }
      return false;
   } // end get
   
   /**
    * Получение ассоциативного массива полей с их значениями
    *
    * @access public
    * @return array
    */
   function get_fields() {
      $fields = array();
      foreach ($this->_values as $name => $value) {
         $fields[$name] = $value['value'];
      }
      return $fields;
   } // end get_fields
   
   /**
    * Сохранение изменений объекта
    *
    * @access public
    * @return boolean
    */
   function update() {
      if ($this->_check()) {
         if ($this->_is_insert) {
            return $this->_insert();
         } else {
            if ($this->_is_changed) {
               return $this->_update();
            }
         }
         return true;
      }
      return false;
   } // end update
   
   /**
    * Удаление объекта из базы
    *
    * @access public
    * @return boolean
    */
   function delete() {
      return $this->_delete();
   } // end delete
   
   /**
    * Получение SQL сегмента ограничения запроса
    *
    * @access protected
    * @param string $key
    * @param mixed $val
    * @package boolean $negative
    * @return string
    */
   function _get_restriction($key, $val, $negative = false) {
      $compare = $negative ? "<>" : "=";
      if (null === $val) {
         $compare = $negative ? "IS NOT" : "IS";
      }
      return $this->db->escape_str($key) . " " . $compare . " " . $this->db->escape($val);
   } // end _get_restriction

   /**
    * Получение ограничения по значениям идентификаторов
    *
    * @access protected
    * @param boolean $negative
    * @return string
    */
   function _get_id_restriction($negative = false) {
      $restr = array();
      foreach ($this->get_id() as $key => $val) {
         array_push($restr, $this->_get_restriction($key, $val, $negative));
      }
      return implode(" AND ", $restr);
   } // end _get_id_restriction
   
   /**
    * Загрузка и инициализация структуры таблицы
    *
    * @access protected
    */
   function _load_structure() {
      $this->_is_changed = false;
      $this->_values = array();
      $this->_primary_keys = array();
      $this->_keys = array();
      $this->_ids = array();
      $structure = $this->db_registry->get_structure($this->_table);
      $fields = $structure['fields'];
      $keys = $structure['keys'];
      // Получаем первичные поля
      if (isset($keys['PRIMARY'])) {
         $this->_primary_keys = $keys['PRIMARY']['fields'];
         foreach ($this->_primary_keys as $key) {
            $this->_ids[$key] = 0;
         }
      } else {
         show_error('The table "' . $this->_table . '" must contain at least one primary key');
      }
      // Уникальные поля
      foreach ($keys as $value) {
         array_push($this->_keys, $value['fields']);
      }
      // Получаем поля
      foreach ($fields as $name => $value) {
         $data = array(
            'value'      => $value['default'],
            'is_changed' => false,
         );
         $this->_values[$name] = array_merge($data, $value);
      }
   } // end _load_structure
   
   /**
    * Загрузка информации по объекту из базы
    *
    * @access protected
    */
   function _load_data() {
      $this->_is_changed = false;
      $sql = "SELECT * FROM " . $this->db->dbprefix($this->_table) . " WHERE " . $this->_get_id_restriction() . " LIMIT 1";
      $query = $this->db->query($sql);
      if (0 < $query->num_rows()) {
         $this->_is_insert = false;
         foreach ($query->row_array() as $name => $value) {
            $this->_values[$name]['value'] = $value;
         }
      } else {
         $this->_is_insert = true;
         foreach ($this->_primary_keys as $key) {
            if ($this->_values[$key]['auto_increment']) {
               $this->_ids[$key] = 0;
            }
         }
         foreach ($this->_values as $name => $value) {
            if (!isset($this->_ids[$name])) {
               $value['value'] = $value['default'];
            } else {
               $value['value'] = $this->_ids[$name];
            }
            $value['is_changed'] = false;
            $this->_values[$name] = $value;
         }
      }
   } // end _load_data
   
   /**
    * Получение титлы по названию поля
    *
    * @access protected
    * @param string $name
    * @return string
    */
   function _get_title($name) {
      $name = str_replace('_', ' ', $name);
      return $name;
   } // end _get_title
   
   /**
    * Базовая проверка валидности полей
    *
    * @access protected
    * @return boolean
    */
   function _check() {
      // Проверяем на валидность полей
      foreach ($this->_values as $name => $value) {
         if (null === $value['value']) {
            // Проверка на не null
            if ($value['not_null'] && !$value['auto_increment']) {
               $error = sprintf($this->lang->line('is_null'), $this->_get_title($name));
               $this->errors->set($name, $error);
               $this->errors->set($this->_table . '_' . $name, $error);
               return false;
            }
         } else {
            // Проверяем поле на соответствие типу
            switch ($value['type']) {
               case 'integer':
                  // TODO: Переделать проверку
                  if (!is_numeric($value['value']) && !$value['auto_increment']) {
                     $error = sprintf($this->lang->line('not_integer'), $this->_get_title($name));
                     $this->errors->set($name, $error);
                     $this->errors->set($this->_table . '_' . $name, $error);
                     return false;
                  }
                  break;
                  
               case 'float':
                  if (!is_numeric($value['value'])) {
                     $error = sprintf($this->lang->line('not_numeric'), $this->_get_title($name));
                     $this->errors->set($name, $error);
                     $this->errors->set($this->_table . '_' . $name, $error);
                     return false;
                  }
                  break;
                  
               case 'string':
                  if (0 < $value['size'] && mb_strlen($value['value'], $this->config->item('charset')) > $value['size']) {
                     $error = sprintf($this->lang->line('is_full'), $this->_get_title($name), $value['size']);
                     $this->errors->set($name, $error);
                     $this->errors->set($this->_table . '_' . $name, $error);
                     return false;
                  }
                  break;
               
               case 'enum':
                  if (!in_array($value['value'], $value['size'])) {
                     $error = sprintf($this->lang->line('not_in'), $this->_get_title($name), implode(', ', $value['size']));
                     $this->errors->set($name, $error);
                     $this->errors->set($this->_table . '_' . $name, $error);
                     return false;
                  }
                  break;
                  
               case 'boolean':
                  if (!is_bool($value['value'])) {
                     $error = sprintf($this->lang->line('not_boolean'), $this->_get_title($name));
                     $this->errors->set($name, $error);
                     $this->errors->set($this->_table . '_' . $name, $error);
                     return false;
                  }
                  break;
            }
         }
      }
      // Проверяем на уникальность
      foreach ($this->_keys as $keys) {
         $restr = array();
         foreach ($keys as $name) {
            if (!$this->_values[$name]['auto_increment']) {
               array_push($restr, $this->_get_restriction($name, $this->get($name)));
            }
         }
         if (0 < count($restr)) {
            $sql = "SELECT * FROM " . $this->db->dbprefix($this->_table) . " WHERE " . implode(" AND ", $restr) . " AND " . $this->_get_id_restriction(true) . " LIMIT 1";
            $query = $this->db->query($sql);
            if (0 < $query->num_rows()) {
               foreach ($keys as $name) {
                  $error = sprintf($this->lang->line('not_unique'), $this->_get_title($name));
                  $this->errors->set($name, $error);
                  $this->errors->set($this->_table . '_' . $name, $error);
               }
               return false;
            }
         }
      }
      return true;
   } // end _check
   
   /**
    * Удаление записи
    *
    * @access protected
    * @return boolean
    */
   function _delete() {
      $sql = "DELETE FROM " . $this->db->dbprefix($this->_table) . " WHERE " . $this->_get_id_restriction() . " LIMIT 1";
      if (null !== $this->db->query($sql)) {
         return true;
      }
      return false;
   } // end _delete
   
   /**
    * Вставка записи
    *
    * @access protected
    * @return boolean
    */
   function _insert() {
      $fields = array();
      $values = array();
      $auto_increment_field = '';
      foreach ($this->_values as $name => $value) {
         if (!$value['auto_increment']) {
            array_push($fields, $this->db->escape_str($name));
            array_push($values, $this->db->escape($value['value']));
         } else {
            $auto_increment_field = $name;
         }
      }
      if (0 < count($fields)) {
         $sql = "INSERT INTO " . $this->db->dbprefix($this->_table) . " (" . implode(", ", $fields) . ") VALUES (" . implode(", ", $values) . ")";
         if (null !== $this->db->query($sql)) {
            // Получаем значение автоинкрементного поля (если это нужно)
            if (!empty($auto_increment_field)) {
               $this->_values[$auto_increment_field]['value'] = $this->db->insert_id();
               if (isset($this->_ids[$auto_increment_field])) {
                  $this->_ids[$auto_increment_field] = $this->_values[$auto_increment_field]['value'];
               }
            }
            $this->_is_changed = false;
            return true;
         }
      }
      return false;
   } // end _insert
   
   /**
    * Изменение записи
    *
    * @access protected
    * @return boolean
    */
   function _update() {
      $values = array();
      foreach ($this->_values as $name => $value) {
         if ($value['is_changed']) {
            array_push($values, $this->db->escape_str($name) . ' = ' . $this->db->escape($value['value']));
         }
      }
      if (0 < count($values)) {
         $sql = "UPDATE " . $this->db->dbprefix($this->_table) . " SET " . implode(", ", $values) . " WHERE " . $this->_get_id_restriction() . " LIMIT 1";
         if (null === $this->db->query($sql)) {
            return false;
         }
      }
      $this->_is_changed = false;
      return true;
   } // end _update
   
}
